package org.xx.armory.commons;

import org.apache.commons.lang3.StringUtils;

import static org.apache.commons.lang3.StringUtils.endsWith;
import static org.apache.commons.lang3.StringUtils.lastIndexOf;
import static org.apache.commons.lang3.StringUtils.startsWithAny;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
import static org.xx.armory.commons.TextUtils.codePointToUtf8;
import static org.xx.armory.commons.Validators.rejectIfBlank;

/**
 * URL相关的工具类。
 */
public class UrlUtils {
    private UrlUtils() {
        throw new AssertionError();
    }

    /**
     * 指定的URL是否以 {@literal http://} 或者 {@literal https://} 或者 {@literal ftp://} 开头。
     *
     * @param url
     *         需判断的URL。
     * @return 此URL是否表示绝对路径。
     * @throws IllegalArgumentException
     *         如果参数 {@code url} 是 {@code null} 或者只包含空白字符。
     */
    public static boolean isAbsoluteUrl(
            String url
    ) {
        url = rejectIfBlank(url, "url").strip();

        return startsWithAny(url, "http://", "https://", "ftp://");
    }

    /**
     * 连接两个URL。
     *
     * <p>两个URL以斜线连接。连接时去掉连接处多余的斜线，或者添加必要的斜线。</p>
     *
     * @param u1
     *         第一个URL。
     * @param u2
     *         第二个URL。
     * @return 连接的结果。
     */
    public static String joinUrl(
            String u1,
            String u2
    ) {
        u1 = trimToEmpty(u1);
        u2 = trimToEmpty(u2);

        if (u1.isBlank()) {
            return u2;
        } else if (u2.isBlank()) {
            return u1;
        } else {
            if (u1.endsWith("/")) {
                return u2.startsWith("/") ? u1 + u2.substring(1) : u1 + u2;
            } else {
                return u2.startsWith("/") ? u1 + u2 : u1 + "/" + u2;
            }
        }
    }

    public static String normUrl(
            String url
    ) {
        url = trimToEmpty(url);

        final var result = new StringBuilder();

        final var cc = url.codePointCount(0, url.length());
        var protocol = "http://";
        var token = new StringBuilder();
        for (var i = 0; i < cc; ++i) {
            final var cp = url.codePointAt(i);
            if (cp == '/') {
                if (endsWith(token, ":")) {
                    token.appendCodePoint(cp);
                } else {
                    if (endsWith(token, ":/")) {
                        protocol = token.append('/').toString();
                        result.setLength(0);
                    } else if (StringUtils.equals(token, ".")) {
                        // 忽略 ./
                    } else if (StringUtils.equals(token, "..")) {
                        // 遇到 ../ 回退一级。
                        if (result.length() > 1) {
                            final var up = lastIndexOf(result, '/', result.length() - 2);
                            if (up > 0) {
                                result.delete(up + 1, result.length());
                            } else {
                                result.setLength(0);
                            }
                        } else {
                            result.setLength(0);
                        }
                    } else if (token.length() > 0) {
                        result.append(token).appendCodePoint('/');
                    }

                    token.setLength(0);
                }
            } else {
                token.appendCodePoint(cp);
            }
        }

        if (token.length() != 0) {
            result.append(token);
        }

        result.insert(0, protocol);

        return result.toString();
    }


    /**
     * 对URL进行<strong>安全地</strong>编码。
     * <p>仅针对空白字符和非ASCII字符进行编码， {@literal +}、{@literal %} 等字符不受影响。</p>
     *
     * @param url
     *         待编码的URL。
     * @return 安全编码的结果。
     * @throws IllegalArgumentException
     *         如果参数 {@code url} 是{@code null}或者只包含空白字符。
     */
    public static String encodeUrlSafely(
            String url
    ) {
        url = rejectIfBlank(url, "uri").trim();

        final var result = new StringBuilder();

        url.codePoints().forEach(ch -> {
            if (Character.isWhitespace(ch)) {
                result.append('+');
            } else if (ch < 0x20 || ch > 0x7f) {
                final var bytes = codePointToUtf8(ch);

                for (final var b : bytes) {
                    result.append('%').append(Integer.toHexString(b & 0xff));
                }
            } else {
                result.appendCodePoint(ch);
            }
        });

        return result.toString();
    }
}
